

import { HmacSHA256, SHA256, enc, lib } from 'crypto-js'

//获取ISO6801格式的时间字符串
const get_iso6801_timestamp = (date: Date) => {
	return date.toISOString().replace(/[:\-]|\.\d{3}Z/g, '') + 'Z';
}

// 获取ISO6801的年月日
const get_yyyymmdd = (date: Date) => {
	return get_iso6801_timestamp(date).split('T')[0]
}


type BucketInfo = {
	name: string,
	region: string,
}


// 所有时间都是用UTC时间，所以签名时需要把时间转换为UTC时间
// https://help.aliyun.com/zh/oss/developer-reference/add-signatures-to-urls?spm=a2c4g.11186623.0.0.7c991da68CDcAR
export class AliOSSv4Signer {
	/**access_key_id */
	#access_key_id: string

	/**access_key_secret */
	#access_key_secret: string

	// js月份从0开始，所以要+1
	// public date: Date = new Date( Date.UTC(2024, 8, 25, 23, 0, 0));

	/**请求日期 */
	public date: Date = new Date();

	/**bucket名称 */
	public bucket: string = ''

	/**桶地域，如 cn-beijing	 */
	public region: string = ''

	/**产品线，云储存为oss */
	public product: string = 'oss'

	/**请求过期时间，单位为秒 */
	public expires: number = 60

	/**请求协议，默认https */
	public protocol: string = 'https'


	constructor(access_key_id: string, access_key_secret: string) {
		this.#access_key_id = access_key_id
		this.#access_key_secret = access_key_secret
	}
	/**HmacSHA256
	 * @param key 密钥
	 * @param msg 待签名消息
	 * @returns 签名结果
	 */
	public static sign(key: lib.WordArray, msg: string,): lib.WordArray {
		return HmacSHA256(msg, key)
	}
	/**
	 * 设置桶信息
	 */
	public set_bucket(bucket_info: BucketInfo) {
		if (bucket_info == undefined) {
			throw new Error('Invalid bucket_info,bucket_info can not be undefined.')
		}
		this.bucket = bucket_info.name.trim()
		this.region = bucket_info.region.trim()
	}

	// 获取签名密钥
	#calc_signing_key(): lib.WordArray {
		const date_ori = new Date()
		const date_yyyymmdd = get_yyyymmdd(date_ori)

		let signing_key = enc.Utf8.parse('aliyun_v4' + this.#access_key_secret)

		signing_key = AliOSSv4Signer.sign(signing_key, date_yyyymmdd)
		signing_key = AliOSSv4Signer.sign(signing_key, this.region)
		signing_key = AliOSSv4Signer.sign(signing_key, this.product)
		signing_key = AliOSSv4Signer.sign(signing_key, 'aliyun_v4_request')

		return signing_key
	}



	// 获取scope
	#get_scope() {
		const date_yyyymmdd = get_yyyymmdd(this.date)
		return date_yyyymmdd + "/" + this.region + "/" + this.product + "/aliyun_v4_request"
	}



	// 获取请求参数
	#get_query_string(): string {

		let query: Array<string> = []

		query.push('x-oss-date=' + get_iso6801_timestamp(this.date))
		query.push('x-oss-expires=' + this.expires)
		query.push('x-oss-signature-version=' + 'OSS4-HMAC-SHA256')
		query.push('x-oss-credential=' + encodeURIComponent(this.#access_key_id + '/' + this.#get_scope()))
		query.sort()
		return query.join('&')
	}

	// 创建规范请求 
	#create_cananical_request(method: string, path: string, headers: any | string = undefined): any {
		method = method.toUpperCase().trim()


		if (!path.startsWith('/')) {
			path = '/' + path
		}

		let cr = ''

		if (headers == undefined) {
			//for direct url
			cr = [
				method,
				'/' + this.bucket + path,
				this.#get_query_string(),
				'',
				'',
				"UNSIGNED-PAYLOAD"
			].join('\n')

			// console.log(`\n############ JS Cananical Request:###${cr}######`)
			return cr
		} else {
			//for wechat miniprogram
			cr = [
				method,
				'/' + this.bucket + path,
				this.#get_query_string(),
				//微信小程序的Request会自动添加content-type则需要设置header
				headers,
				'',
				'',
				"UNSIGNED-PAYLOAD"
			].join('\n')


		}

		return cr

	}

	#create_string_to_sign(canonical_request: string): string {
		const timestamp = get_iso6801_timestamp(this.date)
		const str2sign = "OSS4-HMAC-SHA256\n" +
			timestamp + "\n" +
			this.#get_scope() + "\n" +
			SHA256(canonical_request).toString(enc.Hex)
		return str2sign
	}


	/**异步方法 */
	public async get_signed_url(method: 'GET' | 'PUT', resource_path: string, headers: any): Promise<string> {
		return new Promise((resolve, _) => {

			if ([method, resource_path].includes('')) {
				throw new Error('Invalid parameters,all parameters are required.')
			}

			if ([this.bucket, this.region].includes('')) {
				throw new Error('Invalid properties,bucket and region can not be empty,please run set_bucket() method to set bucket and region.')
			}


			if (!this.region.startsWith('cn-')) {
				throw new Error(`Invalid bucket region,bucket region should not start with cn- like 'cn-beijing'.`)
			}

			// 未指定日期时，默认使用当前时间
			if(this.date === undefined){
			  this.date = new Date()
			}

			// path需要以/开头
			if (!resource_path.trim().startsWith('/')) {
				resource_path = '/' + resource_path.trim()
			}

			//0.计算签名密钥
			const signing_key = this.#calc_signing_key()

			//1.创建规范请求
			const cananical_request = this.#create_cananical_request(
				method.toUpperCase().trim(),
				resource_path,
				headers
			)

			//2.获取字符串to_sign
			const string_to_sign = this.#create_string_to_sign(cananical_request)

			//3.计算签名
			const signature = AliOSSv4Signer.sign(signing_key, string_to_sign).toString(enc.Hex)


			//4.拼接query
			let query = this.#get_query_string() + '&x-oss-signature=' + signature

			//5.拼接url
			let url = `${this.protocol}://${this.bucket}.${this.product}-${this.region}.aliyuncs.com${resource_path}?${query}`
			resolve(url)
		})
	}


	/**
	 * 为浏览器签名
	 * @param resource_path 资源路径
	 * @returns 
	 */
	public async get_signed_direct_url(resource_path: string): Promise<string> {
		return this.get_signed_url('GET', resource_path, undefined)
	}
}


// 保管好access_key_id和access_key_secret，不要放在代码中
const oss = new AliOSSv4Signer('access_key_id', 'access_key_secret')

oss.set_bucket({
	name: 'bucket_name',
	region: 'cn-shanghai'
})

oss.get_signed_direct_url('object_path')
  .then(url=>{
    const a = document.createElement('a')
    a.href = url
    a.target = '_blank'
    a.title = "点击访问"
    a.innerText = url
    document.body.appendChild(a)

})
